[iOS 10] SFSpeechRecognizerで音声認識を試してみた
おばんです、休日は友人の誕生日祝いということでご飯を食べに行きましたが、ふらふら飲み歩いてる経験を活かして店選びをうまくやれて、楽しく過ごすことができました田中です。(よかった(小並感)
さて今回はiOS 10より搭載されたSFSpeechRecognizerによる音声認識をとりあげて書いていきます。
本記事は Apple からベータ版として公開されているドキュメントを情報源としています。 そのため、正式版と異なる情報になる可能性があります。ご留意の上、お読みください。
音声認識って前からできなかったっけ?
フリーのライブラリや外部APIを通して実装することはできました。
大昔に自分も似たようなものを実装していたという記録がありました。(OpenEarsを使ってみた - 情弱spartanの開発日記)
iOS 10からの音声認識概要
公式には以下のWWDCのビデオで説明されています。 Speech Recognition API
ビデオの内容を簡単に要約すると
iOSの音声認識のこれまで
- iPhone 4S時代からキーボードの中に音声認識あった
- iOS5からあった
- 使うのにKeyboardを必要とした
- リアルタイム音声のインプットのみ受付
- 言語のカスタマイズが出来なかった
iOS 10からの音声認識
- Speech Recognition API
- Siriに使われているのと同じものが使える
- リアルタイムのみならず、録音済みの音声データも音声認識可能に
注意点
リソースに関して
- 端末ごとの音声認識回数には制限がある
- アプリごとにも認識回数に制限がある
- バッテリーとネットワークに関してハイコスト
- 一回のディクテーション時間のMaxは1分
プライバシーとユーザビリティに関して
- ユーザーが録音中だとわかるUIを作ろう
- パスワード、ヘルス情報、経済などの機密情報は音声認識させない
- エラーをユーザーに伝える
実装方法
公式が出している以下のサンプルコードの内容を写経しつつ、解析をしていきます。 SpeakToMe: Using Speech Recognition with AVAudioEngine
Info.plistの編集
iOS 10からはプライバシーアクセスに関するものはInfo.plistにその用途を記述しないと強制終了します。 [iOS 10] 各種ユーザーデータへアクセスする目的を記述することが必須になるようです
今回必要になるのはNSMicrophoneUsageDescription(マイクアクセス)
とNSSpeechRecognitionUsageDescription(音声認識アクセス)
。
以下のように記述しましょう。
<dict> <key>NSMicrophoneUsageDescription</key> <string>マイクの用途について記載します。</string> <key>NSSpeechRecognitionUsageDescription</key> <string>音声認識の用途について記載します。</string> ......... ...... ... </dict>
importする
import Speech
用意するプロパティとdelegateの設定
class ViewController: UIViewController { // "ja-JP"を指定すると日本語になります。 private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))! private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? private var recognitionTask: SFSpeechRecognitionTask? private let audioEngine = AVAudioEngine() @IBOutlet weak var label: UILabel! @IBOutlet weak var button: UIButton! public override func viewDidLoad() { super.viewDidLoad() speechRecognizer.delegate = self button.isEnabled = false } } extension ViewController: SFSpeechRecognizerDelegate { // 音声認識の可否が変更したときに呼ばれるdelegate func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) { if available { button.isEnabled = true button.setTitle("音声認識スタート", for: []) } else { button.isEnabled = false button.setTitle("音声認識ストップ", for: .disabled) } } }
言語設定の対応リストは58種類のようです。以下を参考にさせていただきました。 【iOS 10】Speechフレームワークで音声認識 - 対応言語リスト付き - Over&Out その後
認証
本家のサンプルはviewDidAppearに直接書いていましたが、とりあえず分離させました。
public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) requestRecognizerAuthorization() } private func requestRecognizerAuthorization() { // 認証処理 SFSpeechRecognizer.requestAuthorization { authStatus in // メインスレッドで処理したい内容のため、OperationQueue.main.addOperationを使う OperationQueue.main.addOperation { [weak self] in guard let `self` = self else { return } switch authStatus { case .authorized: self.button.isEnabled = true case .denied: self.button.isEnabled = false self.button.setTitle("音声認識へのアクセスが拒否されています。", for: .disabled) case .restricted: self.button.isEnabled = false self.button.setTitle("この端末で音声認識はできません。", for: .disabled) case .notDetermined: self.button.isEnabled = false self.button.setTitle("音声認識はまだ許可されていません。", for: .disabled) } } } }
音声認識の処理
private func startRecording() throws { refreshTask() let audioSession = AVAudioSession.sharedInstance() // 録音用のカテゴリをセット try audioSession.setCategory(AVAudioSessionCategoryRecord) try audioSession.setMode(AVAudioSessionModeMeasurement) try audioSession.setActive(true, with: .notifyOthersOnDeactivation) recognitionRequest = SFSpeechAudioBufferRecognitionRequest() guard let inputNode = audioEngine.inputNode else { fatalError("Audio engine has no input node") } guard let recognitionRequest = recognitionRequest else { fatalError("Unable to created a SFSpeechAudioBufferRecognitionRequest object") } // 録音が完了する前のリクエストを作るかどうかのフラグ。 // trueだと現在-1回目のリクエスト結果が返ってくる模様。falseだとボタンをオフにしたときに音声認識の結果が返ってくる設定。 recognitionRequest.shouldReportPartialResults = true recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in guard let `self` = self else { return } var isFinal = false if let result = result { self.label.text = result.bestTranscription.formattedString isFinal = result.isFinal } // エラーがある、もしくは最後の認識結果だった場合の処理 if error != nil || isFinal { self.audioEngine.stop() inputNode.removeTap(onBus: 0) self.recognitionRequest = nil self.recognitionTask = nil self.button.isEnabled = true self.button.setTitle("音声認識スタート", for: []) } } // マイクから取得した音声バッファをリクエストに渡す let recordingFormat = inputNode.outputFormat(forBus: 0) inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in self.recognitionRequest?.append(buffer) } try startAudioEngine() } private func refreshTask() { if let recognitionTask = recognitionTask { recognitionTask.cancel() self.recognitionTask = nil } } private func startAudioEngine() throws { // startの前にリソースを確保しておく。 audioEngine.prepare() try audioEngine.start() label.text = "どうぞ喋ってください。" }
音声認識の開始と終了
@IBAction func tappedStartButton(_ sender: AnyObject) { if audioEngine.isRunning { audioEngine.stop() recognitionRequest?.endAudio() button.isEnabled = false button.setTitle("停止中", for: .disabled) } else { try! startRecording() button.setTitle("音声認識を中止", for: []) } }
ここまでで音声認識は実行可能となります。
まとめ
オフィスでデバッグしてたんですが、なかなか恥ずかしいですね。 急に「こんにちは、あ、あ、あーーー」とか喋り出すと「暑さでやられてしまったのか」と心配されました。
個人的にはこれを待っていたんだという感じです! 日本語の精度も高く、どうやらユーザーに適応していってくれる機能が含まれているようなので、使えば使うほど良いのでしょうか。 ただ処理結果が、
僕「マイクのテスト中です」 iOS「米です」
とか言われたときはどうしてやろうかと思いました。 お米食べろ!!!!!
この辺の技術と組み合わせると、品詞分解してくれるところまでだけでもちょっと楽しいんじゃないかなとか思ってます。今度実験的に試したい。 Swift での自然言語処理 - Realm
参考
- iOSアプリでの音声認識機能実装方法まとめ - Qiita
- フリーの iOS 向け音声認識/音声合成ライブラリ『OpenEars』の使い方
- OpenEarsを使ってみた - 情弱spartanの開発日記
- Speech Recognition API
- SpeakToMe: Using Speech Recognition with AVAudioEngine
- [iOS 10] 各種ユーザーデータへアクセスする目的を記述することが必須になるようです
- 【iOS 10】Speechフレームワークで音声認識 - 対応言語リスト付き - Over&Out その後
- Swift での自然言語処理 - Realm
- 【iOS 10】Speechフレームワークで音声認識 - 対応言語は58種類! - Qiita